Repository navigation
fix(ai): stated provider flags win, keys for two providers stop, github never inferred - #857
Merged
Merged
Conversation
With OPENAI_API_KEY and ANTHROPIC_API_KEY both set (or one provider's key and OLLAMA_HOST) and nothing naming a provider, a run without a TTY skipped both keys and used a running local Ollama. Founder decision (2026-11-12): stop and name the keys. _infer_provider_from_ambient now returns None first when the key variables name two or more providers, so no Ollama probe runs and no agent is picked. Forge raises copilot_no_llm_provider with no_provider_message, and fluid doctor (check_llm_readiness) reports the same. The saved fluid ai setup choice and exactly one keyring key still come first. The message moves into ambiguous_keys_message for the callers that follow.
Interactive setup's step 2 took any PROVIDER_ENV_VARS variable, so GITHUB_API_KEY alone selected github, which setup then failed to build (KeyError: 'github'), though PROVIDER_ENV_VARS says github is used only when named. It also missed GEMINI_API_KEY, and with two keys it used the first. Step 2 now reads the key variables a run without a TTY reads (_explicit_key_signals): one provider's key selects it, GOOGLE_API_KEY before GEMINI_API_KEY as the resolver reads them, and OLLAMA_HOST is left to step 3. Keys for two or more providers select none: exactly one keyring key then decides, as in the ladder, else setup prints ambiguous_keys_message and stops. fluid ai test and fluid init's AI check follow the same rule.
…dpoint After run_ai_setup_inline, forge and init's AI path overwrote args.llm_provider/llm_model/llm_endpoint with setup's pick, so --llm-provider, --llm-model, --llm-endpoint and FLUID_LLM_PROVIDER, FLUID_FORGE_AGENT, FLUID_LLM_MODEL and FLUID_LLM_ENDPOINT lost to the saved or first-found provider. A run that names its provider (named_llm_provider) now skips inline setup and resolves as a run without a TTY does, so the named provider gets its catalog model unless --llm-model is given. Otherwise fill_llm_args_from_setup fills only what is unstated, records the pick as not named, and drops an agent pick when the run states an endpoint or a model or runs the agent loop. The welcome and interview panels name the provider the run uses, and init's handoff counts a named provider as set up.
fluid ai test read only --llm-provider, so with FLUID_LLM_PROVIDER=anthropic it tested the saved provider, which forge would not use. It now reads the provider the run names as forge does (named_llm_provider), and that provider gets FLUID_LLM_API_KEY, as with the flag. A coding agent or mcp-sampling named either way is not tested, and the command says so instead of failing with "Unsupported AI provider 'claude_code'".
…ion-flags-7c2edc The only conflict was CHANGELOG.md: 0.29.0 moved [Unreleased] under [0.29.0] — 2026-11-12. This branch's four entries go into the empty [Unreleased] above it, under Changed and Fixed.
… any spelling Follow-ups from the defect scan of this branch: - fluid init's post-init check (_is_ai_configured) imported llm.provider_resolve before llm.providers. The two modules import each other, so in a plain `fluid init`, which loads neither earlier, the check raised ImportError and the "create your first data product?" offer was dropped. The helpers are now re-exported from llm.providers and imported from there in init, fluid ai test and interactive setup. A test runs each entry point in a fresh interpreter. - fluid ai test's "does not test" guard missed spellings forge accepts (MCP-Sampling, Mcp_Sampling). - init's fallback now says "No AI provider selected": setup also returns nothing when keys for two providers are set, after naming them.
Defect-scan follow-up. With key variables for two or more providers set, interactive setup names them and chooses none; fluid init's AI path then fell through to its no-AI branch and wrote the customer-360 template with exit code 0, in CI as well. It now stops with exit code 1, as forge does without a TTY. With nothing configured it still offers the template. fluid ai test's message for mcp-sampling now says the IDE answers those calls, since fluid doctor does not check that provider.
ripwire --quality-delta on this branch gated on complexity and length the branch had added to run_ai_setup_inline (76 to 97), _resolve_ai_test_config (41 to 48, 79 to 118 lines) and init._ai_mode (21 to 26), and on test duplication from one delenv loop pasted into four tests. No behaviour changes: - ai_setup: step 2 is _setup_from_key_variables, which returns a config, _STOP_SETUP or None. - _ai_setup_test: _untested_agent_message and _key_variable_choice; the timeout is set once, inside _build_for_provider. - init: _setup_ai_mode_llm returns "ready", "stop" or "template". - tests: a no_llm_key_variables fixture in conftest replaces the loops.
The only conflict was CHANGELOG.md, where both sides added [Unreleased] entries. Every entry is kept: this branch's Changed entry, then one Fixed list with #856's two entries and this branch's three.
Code quality (py/import-and-import-from) flagged fluid_build.llm.providers and fluid_build.cli.ai_setup: each was imported as a module and with a from-import. Both are now module imports at the top of the file, and the names are qualified (providers.LlmConfig, ai_setup.run_ai_setup_inline). No test checks anything different.
# Conflicts: # CHANGELOG.md
fas89
enabled auto-merge (squash)
November 12, 2026 21:49
# Conflicts: # CHANGELOG.md
2 of 3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Design note.
fluid forge,fluid init,fluid ai testorfluid doctoron a machine that may hold key variables, a savedfluid ai setupchoice, keyring keys and a local Ollama. Also CI runs without a TTY. Command Center always names its provider.--llm-providerorFLUID_LLM_PROVIDER) and every command uses exactly that provider with its own key. Nothing you state (provider, model, endpoint) is replaced by a setup pick.fluid ai testshare it, and there are no new flags or variables.ai testexits 2.Four provider-selection fixes, one commit each. After those comes a merge of main (the 0.29.0 changelog cut), then two commits from this PR's defect scan (
effbae02,523cb579), one behaviour-preserving refactor thatripwire --quality-deltaasked for (ee5c679c), and a merge of main after #856 (c30f4476, CHANGELOG entries from both sides kept). The founder confirmed the edge cases on 2026-11-12.1. Keys for two or more providers stop the run (
6b65247a).Before: with
OPENAI_API_KEYandANTHROPIC_API_KEYboth set and nothing naming a provider, a run without a TTY skipped both keys and used a running local Ollama. The same happened with one key plusOLLAMA_HOST. Founder decision: stop and name the keys.Now
provider_resolve._infer_provider_from_ambientreturns no provider before it probes Ollama when the key variables name two or more providers. Forge stops withcopilot_no_llm_provider, for example "No LLM provider is selected: OPENAI_API_KEY, ANTHROPIC_API_KEY are all set, so none of them is chosen. Pick one with --llm-provider or FLUID_LLM_PROVIDER."fluid doctorreports the same. The savedfluid ai setupchoice and exactly one keyring key still come first, as before.GEMINI_API_KEYwithGOOGLE_API_KEYcounts as one provider.2. Interactive setup and
fluid ai testread the ladder's key variables, never github (99b9be7c).Before: inline setup's step 2 took any
PROVIDER_ENV_VARSvariable.GITHUB_API_KEYalone selected github, which setup then failed to build (KeyError: 'github'). The comment onPROVIDER_ENV_VARSsays github is only used when named.GEMINI_API_KEY.Now step 2 reads the same key variables as a run without a TTY (
_explicit_key_signals):GOOGLE_API_KEYread beforeGEMINI_API_KEYas the resolver does;OLLAMA_HOSTis left to step 3;fluid ai testandfluid init's "is AI configured" check follow the same rule. GitHub Models stays reachable with--llm-provider github.Also in this commit:
tests/conftest.py::_session_env_is_restored. The import tests' canary key leaked intoos.environthroughset_session_env, and later tests then selected openai..secrets.baselinechange at HEAD. Commit 2 refreshed one line number, and the refactor moved that line back, so the merge of fix(ai,receipts): ai models --provider takes the LLM registry; run ids never repeat #856 restores main's file.3. Interactive
fluid forgeandfluid initkeep the provider, model and endpoint you state (636eb62a).Before: after
run_ai_setup_inline, forge overwroteargs.llm_provider,args.llm_modelandargs.llm_endpointwith setup's pick, and so did init's AI path. Sofluid forge --llm-provider ollamacould call Anthropic. The same went forFLUID_LLM_PROVIDER,FLUID_FORGE_AGENT,FLUID_LLM_MODELandFLUID_LLM_ENDPOINT.Now:
providers.named_llm_provider) skips inline setup and resolves as a run without a TTY does. The named provider gets its catalog model unless--llm-modelis given (fix(ai): saved AI choice in non-interactive runs, JSON-only --agent stdout, mission agent loop #854's rule: a named provider takes nothing from the saved choice).providers.fill_llm_args_from_setupfills only what the run leaves unstated, and records the pick as not named, soFLUID_LLM_API_KEYstays scoped (security(ai): FLUID_LLM_API_KEY only for a named provider; saved keys stay off project hosts #853).fluid init's handoff no longer offers AI setup when a provider is named.4.
fluid ai testhonoursFLUID_LLM_PROVIDERandFLUID_FORGE_AGENT(99933fe6).Before: it read only
--llm-provider, so withFLUID_LLM_PROVIDER=anthropicit tested the saved provider, which forge would not use.Now it reads the provider the run names, as forge does, and that provider gets
FLUID_LLM_API_KEY, as with the flag. A coding agent ormcp-samplingnamed either way is not tested, and the command says so. Before,claude-codefailed as "Unsupported AI provider 'claude_code'".Fixes from the PR gate (defect scan of this diff):
effbae02:fluid init's post-init check importedllm.provider_resolvebeforellm.providers. The two modules import each other, so in a plainfluid init, which loads neither before that check, it raisedImportError, and the "create your first data product?" offer was dropped. Main is fine here only because it did not import that module there. The helpers are now re-exported fromllm.providersand imported from there. A test runs each entry point (init,ai test, setup) in a fresh interpreter. The same commit makesfluid ai test's "does not test" guard accept every spelling forge accepts (MCP-Sampling,Mcp_Sampling).523cb579: with key variables for two or more providers,fluid init's AI path used to write the customer-360 template and exit 0, in CI too. It now exits 1 after setup has named the keys, as forge does without a TTY. Please confirm this choice: the alternative is to keep the template fallback, as interactive forge keeps its "Continue with guided mode (no AI)?" offer. With nothing configured, init still offers the template.Command Center is unaffected.
fluid_cli_service.forge_from_intentalways passes--llm-providerwith that provider's own key variable; checked at CCorigin/main88067918. CC'sdoctorcall does not read the AI-readiness rows. CC's exact drafting argv (--llm-provider anthropic, withOPENAI_API_KEYandANTHROPIC_API_KEYboth set) still uses anthropic, checked live.Prior art (borrow receipts):
-m/--keybeatsLLM_MODELand the env key, which beat the saved default (docs/setup.md). Fix 3 has the same precedence.aider/onboarding.py::try_to_select_default_modeltakes the first key it finds in a fixed order, which ignores the others silently. Fix 1 diverges from this on purpose, by the founder's decision.init_chat_modelraisesValueErrorrather than guessing a provider it cannot infer. Google's Gemini docs sayGOOGLE_API_KEYwins when both Gemini variables are set, which step 2 follows.Review verdicts (/ship)
99933fe6..HEAD) found none either.FLUID_LLM_PROVIDER).effbae02,523cb579).effbae02.forge_modes.run_ai_copilot_modepreflights before any keyring read). The oldPROVIDER_ENV_VARSloops are gone, with no dual path left. Overall verdict: PASS.--llm-provider, whichfluid initlacks (FLUID_LLM_PROVIDERworks);fluid ai testadds its generic "Runfluid ai setup" hint even for a named agent.First run: 36 gating rows. The refactor in
ee5c679cremoved the complexity and length growth inrun_ai_setup_inline,_resolve_ai_test_configandinit._ai_mode, and the test duplication.Still gating:
LlmConfigbuilders, env-cleaning loops,Namespacehelpers);_stated_argidiom;None is a production-code regression. Changing them would only game the number.
Type of Change
--llm-provider <name>orFLUID_LLM_PROVIDER=<name>.OLLAMA_HOSTtogether with a cloud key now counts as two providers.fluid forge --llm-provider Xnow uses X with its catalog model, not the saved provider's model.Documentation
forge_docs pages to update:
docs/advanced/llm-providers.mdOLLAMA_HOSTcounts as a key variable. The line-113 sentence "an Ollama that answers ... is still used" changes.GEMINI_API_KEY, and github is used only when named.docs/cli/forge.md: interactive runs keep--llm-provider/--llm-model/--llm-endpointand theirFLUID_LLM_*twins.docs/cli/ai.md:fluid ai testhonoursFLUID_LLM_PROVIDER/FLUID_FORGE_AGENT, which getFLUID_LLM_API_KEY;mcp-sampling;docs/cli/doctor.md: readiness says "Not selected" and names the keys when two providers' keys are set.docs/advanced/environment-variables.md:FLUID_LLM_PROVIDER/FLUID_FORGE_AGENTapply tofluid ai testtoo;OLLAMA_HOSTtakes part in the two-provider rule.docs/advanced/production-troubleshooting.md:copilot_no_llm_providerwith "... are all set" now appears even while Ollama runs.docs/upgrading.md: bullets under "On forge-climainsince 0.29.0 (not yet released)" for the three breaking points above.Checklist
pytest)ruff checkandblackwith no errorspython scripts/add_license_headers.py;examples/**exempt)Testing
New
tests/test_llm_provider_selection_flags.py: 43 test functions, 50 cases. On origin/main, 37 cases fail and 13 regression guards pass. Each of the four fix commits was replayed on a clean origin/main checkout and is green on its own: 616, 627, 642, then 648 related tests.Full suite on the four fix commits: 34,884 passed. A rerun on this head is in progress, and CI runs the suite too. The 13 failures are the shared venv's known baseline:
/var: 2;test_lazy_core_commands: 2, because the probe imports the venv's editable install;test_startup_budget: 5.One more,
test_iac_databricks_metadata_fake_e2e, failed on a broken pipe to its local fake server under load and passes alone. Before the two gate-fix commits, the 112 related test files passed (2716). After them, the three most affected files passed: 137.Startup budget in a lean
.[dev,local]venv installed from this branch: 9 passed (build_parser()299/314,discover_providers()188/198,fluid providers311/327).Gates:
tests/test_license_headers.py: pass. lint-imports: 6 contracts kept.python:3.12-slim(CI's recipe) with the baseline left unchanged.Reviews:
FLUID_LLM_PROVIDER);effbae02/523cb579;--llm-provider,FLUID_LLM_PROVIDER,FLUID_FORGE_AGENT), so a project.envcannot unlockFLUID_LLM_API_KEY. Setup's pick is still recorded as not named. The ambiguity branches send no operator key to a project-controlled host.Live A/B through the
fluidCLI against main:env -i, a tempHOME, the null keyring,HTTP(S)_PROXYat a dead port (no egress), aPATHwithout agent CLIs, and a loopback stub that logs the path, model and key tail of each request.fluid doctorwith both keys and the real Ollama visible: main reportsollamaready, this branch "Not selected" plus the message.forge --non-interactivewith both keys andOLLAMA_HOSTat the stub: main sent 2 generations to Ollama; this branch stops naming all three variables, and the stub sees 0 requests.GITHUB_API_KEYonly. Interactive forge on main prints "Using github from environment." thenKeyError: 'github'; this branch infers nothing.fluid ai test: main "Unsupported AI provider 'github'", this branch "No AI provider configured".fluid forge --llm-provider ollama --llm-model stated-model-x --llm-endpoint <stub>, with a saved Anthropic and its key. Main printed "Using Anthropic (Claude) from environment." and called Anthropic; this branch sent its 2 requests to the stub withstated-model-x. The same holds withFLUID_LLM_PROVIDER/FLUID_LLM_MODEL/FLUID_LLM_ENDPOINT.--llm-modelwith a saved Ollama: main sent the saved model, this branch the stated one.FLUID_LLM_PROVIDER=anthropicwith a saved openai: main tested openai, this branch anthropic.FLUID_FORGE_AGENT=claude-codegets the "does not test" message.Follow-ups found, all pre-existing (same on main) and not fixed here:
--llm-endpoint/FLUID_LLM_ENDPOINTnever reach litellm for hosted providers.litellm_backend._apply_ollama_api_basesetsapi_basefor Ollama only, so--llm-provider openai --llm-endpoint <host>still calls api.openai.com. This was checked live.--non-interactiveprints a full traceback before the error line.fluid ai testuses the saved model for a named provider. It also tries the saved choice before a single key variable (the ladder does the reverse), and readsGEMINI_API_KEYbeforeGOOGLE_API_KEY(forge readsGOOGLE_API_KEYfirst).fluid doctorreports "Unknown LLM provider" forclaude_code,Claude-Codeandmcp-sampling, although forge accepts them.init._offer_first_forgecopiesargs.llm_providerinto a new forge Namespace without thellm_provider_namedrecord. If that handoff ever ran after_ai_modefilled in setup's pick, the pick would count as named forFLUID_LLM_API_KEY. Today it does not run there: a written contract skips it, and a declined preview returns 1.--llm-modelwins. The next line, "AI: …", is correct.